<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>跌倒检测可视化面板</title>
  <link rel="preconnect" href="https://cdn.jsdelivr.net" />
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
  <style>
    :root { --bg:#fafafa; --fg:#222; --muted:#666; --accent:#2563eb; }
    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: var(--bg); color: var(--fg); }
    .wrap { max-width: 980px; margin: 24px auto; padding: 0 16px; }
    h1 { font-size: 22px; margin: 8px 0 16px; }
    .grid { display: grid; grid-template-columns: 1fr; gap: 16px; }
    @media (min-width: 900px) { .grid { grid-template-columns: 1fr 1fr; } }
    .card { background: #fff; border: 1px solid #eee; border-radius: 10px; padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,.04); }
    .row { display: flex; gap: 8px; align-items: center; }
    input, button, select { padding: 8px 10px; border-radius: 8px; border: 1px solid #ddd; }
    button { background: var(--accent); color: #fff; border: none; }
    table { width: 100%; border-collapse: collapse; font-size: 14px; }
    th, td { border-bottom: 1px solid #eee; padding: 6px; text-align: left; }
    .muted { color: var(--muted); }
  </style>
</head>
<body>
<div class="wrap">
  <div class="row" style="justify-content: space-between;">
    <h1>跌倒检测可视化面板</h1>
    <button onclick="window.location.href='http://localhost:5173/'">返回首页</button>
  </div>
  <div class="grid">
    <div class="card">
      <h3>近30天事件统计</h3>
      <canvas id="dailyChart" height="140"></canvas>
    </div>
    <div class="card">
      <h3>最近事件加速度幅值</h3>
      <canvas id="magChart" height="140"></canvas>
    </div>
  </div>

  <div class="grid" style="margin-top:16px;">
    <div class="card">
      <h3>类别分布（Fall-Down-Cls-v1）</h3>
      <canvas id="classPie" height="160"></canvas>
    </div>
    <div class="card">
      <h3>置信度直方图</h3>
      <canvas id="confHist" height="160"></canvas>
    </div>
  </div>

  <div class="card" style="margin-top:16px;">
    <h3>事件列表</h3>
    <table>
      <thead>
        <tr><th>时间</th><th>设备</th><th>类型</th><th>置信度</th><th>幅值</th><th>备注</th></tr>
      </thead>
      <tbody id="evtBody"></tbody>
    </table>
  </div>

  <div class="card" style="margin-top:16px;">
    <h3>数据导入（Fall-Down-Cls-v1 CSV）与手动录入</h3>
    <div class="row" style="margin-bottom:8px;">
      <input id="csvPath" type="text" placeholder="例如 data/Fall-Down-Cls-v1.csv" style="flex:1;" />
      <button id="importCsv">导入CSV</button>
      <span id="importTip" class="muted"></span>
    </div>
    <div class="row">
      <input id="conf" type="number" step="0.01" min="0" max="1" value="0.8" placeholder="置信度" />
      <input id="mag" type="number" step="0.01" min="0" max="5" value="2.0" placeholder="加速度幅值" />
      <input id="dev" type="text" value="pi-accel-1" placeholder="设备ID" />
      <input id="note" type="text" value="客厅" placeholder="备注" />
      <button id="submit">提交事件</button>
      <span id="tip" class="muted"></span>
    </div>
  </div>
</div>

<script>
async function fetchJSON(url) {
  const res = await fetch(url);
  return res.json();
}

async function loadDaily() {
  const data = await fetchJSON('/api/stats/daily?days=30');
  const ctx = document.getElementById('dailyChart');
  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: data.labels,
      datasets: [{ label: '跌倒次数', data: data.values, backgroundColor: '#60a5fa' }]
    },
    options: { scales: { y: { beginAtZero: true } } }
  });
}

async function loadClassDist() {
  const data = await fetchJSON('/api/stats/class-dist');
  const ctx = document.getElementById('classPie');
  new Chart(ctx, {
    type: 'pie',
    data: {
      labels: data.labels,
      datasets: [{ data: data.values, backgroundColor: ['#ef4444','#10b981','#60a5fa','#f59e0b'] }]
    }
  });
}

async function loadEvents() {
  const evs = await fetchJSON('/api/events');
  const tbody = document.getElementById('evtBody');
  tbody.innerHTML = '';
  const last = evs.slice(-40); // 最近40条
  for (const e of last.reverse()) {
    const tr = document.createElement('tr');
    tr.innerHTML = `<td>${e.ts}</td><td>${e.device||''}</td><td>${e.type||''}</td><td>${(e.confidence||0).toFixed(2)}</td><td>${(e.magnitude||0).toFixed(2)}</td><td>${e.note||''}</td>`;
    tbody.appendChild(tr);
  }
  // 幅值图
  const ctx2 = document.getElementById('magChart');
  new Chart(ctx2, {
    type: 'line',
    data: {
      labels: last.map(e => e.ts),
      datasets: [{ label: '加速度幅值(g)', data: last.map(e => e.magnitude||0), borderColor: '#22c55e', fill: false }]
    },
    options: { plugins: { legend: { display: true } }, scales: { y: { beginAtZero: true } } }
  });
}

async function loadConfHist() {
  const data = await fetchJSON('/api/stats/conf-hist?bins=20');
  const ctx = document.getElementById('confHist');
  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: data.labels,
      datasets: [{ label: '置信度', data: data.values, backgroundColor: '#a78bfa' }]
    },
    options: { scales: { y: { beginAtZero: true } } }
  });
}

async function submitEvent() {
  const payload = {
    type: 'fall',
    confidence: Number(document.getElementById('conf').value),
    magnitude: Number(document.getElementById('mag').value),
    device: document.getElementById('dev').value,
    note: document.getElementById('note').value
  };
  const tip = document.getElementById('tip');
  try {
    const res = await fetch('/api/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
    const json = await res.json();
    if (json.ok) {
      tip.textContent = '提交成功';
      await loadDaily();
      await loadClassDist();
      await loadConfHist();
      await loadEvents();
    } else {
      tip.textContent = '提交失败';
    }
  } catch (e) {
    tip.textContent = '提交异常';
  }
}

async function importCsv() {
  const path = document.getElementById('csvPath').value.trim();
  const tip = document.getElementById('importTip');
  if (!path) { tip.textContent = '请输入CSV路径'; return; }
  try {
    const res = await fetch('/api/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }) });
    const json = await res.json();
    if (json.ok) {
      tip.textContent = `导入成功：${json.imported} 条`;
      await loadDaily();
      await loadClassDist();
      await loadConfHist();
      await loadEvents();
    } else {
      tip.textContent = '导入失败';
    }
  } catch (e) {
    tip.textContent = '导入异常';
  }
}

document.getElementById('submit').addEventListener('click', submitEvent);
document.getElementById('importCsv').addEventListener('click', importCsv);
loadDaily();
loadClassDist();
loadConfHist();
loadEvents();
</script>
</body>
</html>

<div class="grid" style="margin-top:16px;">
  <div class="card">
    <h3>图像数据导入（VOC/TXT）</h3>
    <div class="row" style="margin-bottom:8px;">
      <input id="imgBase" type="text" style="flex:1;" value="C:/Users/lxh/Desktop/FallDataset" placeholder="数据集根目录（可选）" />
    </div>
    <div class="row" style="margin-bottom:8px;">
      <input id="vocImages" type="text" style="flex:1;" value="images" placeholder="images 子目录或绝对路径" />
      <input id="vocAnn" type="text" style="flex:1;" value="annotations/voc" placeholder="VOC 标注目录" />
      <button id="importVoc">导入VOC</button>
      <span id="vocTip" class="muted"></span>
    </div>
    <div class="row">
      <input id="txtImages" type="text" style="flex:1;" value="images" placeholder="images 子目录或绝对路径" />
      <input id="txtAnn" type="text" style="flex:1;" value="annotations/txt" placeholder="TXT 标注目录" />
      <button id="importTxt">导入TXT</button>
      <span id="txtTip" class="muted"></span>
    </div>
  </div>
  <div class="card">
    <h3>图像类别分布</h3>
    <canvas id="imgClassPie" height="160"></canvas>
  </div>
</div>

<div class="card" style="margin-top:16px;">
  <h3>图像预览（前 30 张）</h3>
  <div id="imgGrid" style="display:grid; grid-template-columns: repeat(3, 1fr); gap:10px;"></div>
</div>

<script>
async function loadImgClassDist(){
  const data = await fetchJSON('/api/stats/images/class-dist');
  const ctx = document.getElementById('imgClassPie');
  new Chart(ctx, {type:'pie', data:{labels:data.labels, datasets:[{data:data.values, backgroundColor:['#ef4444','#10b981','#60a5fa','#f59e0b']}]} });
}

function makeOverlayBox(imgEl, box){
  const wrap = document.createElement('div');
  wrap.style.position='relative';
  wrap.style.width='100%';
  const image = document.createElement('img');
  image.src = `/api/images/preview?path=${encodeURIComponent(box.path || imgEl.srcPath)}`;
  image.style.width='100%';
  image.style.display='block';
  wrap.appendChild(image);
  image.onload = () => {
    const W = image.naturalWidth;
    const H = image.naturalHeight;
    const overlay = document.createElement('div');
    overlay.style.position='absolute';
    overlay.style.pointerEvents='none';
    overlay.style.left='0'; overlay.style.top='0'; overlay.style.right='0'; overlay.style.bottom='0';
    wrap.appendChild(overlay);
    for(const b of box.boxes||[]){
      let left, top, width, height;
      if(b.rel){
        left = ((b.cx - b.w/2) * 100).toFixed(2)+'%';
        top = ((b.cy - b.h/2) * 100).toFixed(2)+'%';
        width = (b.w * 100).toFixed(2)+'%';
        height = (b.h * 100).toFixed(2)+'%';
      } else {
        left = ((b.x1 / W) * 100).toFixed(2)+'%';
        top = ((b.y1 / H) * 100).toFixed(2)+'%';
        width = (((b.x2 - b.x1) / W) * 100).toFixed(2)+'%';
        height = (((b.y2 - b.y1) / H) * 100).toFixed(2)+'%';
      }
      const rect = document.createElement('div');
      rect.style.position='absolute';
      rect.style.border='2px solid #f59e0b';
      rect.style.borderRadius='2px';
      rect.style.left=left; rect.style.top=top; rect.style.width=width; rect.style.height=height;
      overlay.appendChild(rect);
    }
  };
  return wrap;
}

async function loadImagesGrid(){
  const grid = document.getElementById('imgGrid');
  grid.innerHTML='';
  const res = await fetchJSON('/api/images?page=1&size=30');
  for(const it of res.items){
    const card = document.createElement('div');
    card.style.border='1px solid #eee'; card.style.borderRadius='8px'; card.style.padding='6px';
    const title = document.createElement('div');
    title.textContent = `${it.label} (${(it.confidence||1).toFixed(2)})`;
    title.className='muted';
    const imgEl = {srcPath: it.path, boxes: it.boxes};
    card.appendChild(makeOverlayBox(imgEl, {path: it.path, boxes: it.boxes}));
    card.appendChild(title);
    grid.appendChild(card);
  }
}

async function importVoc(){
  const base = document.getElementById('imgBase').value.trim();
  const images = document.getElementById('vocImages').value.trim();
  const ann = document.getElementById('vocAnn').value.trim();
  const tip = document.getElementById('vocTip');
  if(base) { window.FALL_IMG_BASE = base; }
  try {
    const payload = { images_dir: images, voc_dir: ann };
    const res = await fetch('/api/images/import-voc', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
    const json = await res.json();
    if(json.ok){ tip.textContent = `VOC导入成功：${json.imported} 张`; await loadImgClassDist(); await loadImagesGrid(); }
    else { tip.textContent = 'VOC导入失败'; }
  } catch(e){ tip.textContent = 'VOC导入异常'; }
}

async function importTxt(){
  const images = document.getElementById('txtImages').value.trim();
  const ann = document.getElementById('txtAnn').value.trim();
  const tip = document.getElementById('txtTip');
  try {
    const payload = { images_dir: images, txt_dir: ann };
    const res = await fetch('/api/images/import-txt', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
    const json = await res.json();
    if(json.ok){ tip.textContent = `TXT导入成功：${json.imported} 张`; await loadImgClassDist(); await loadImagesGrid(); }
    else { tip.textContent = 'TXT导入失败'; }
  } catch(e){ tip.textContent = 'TXT导入异常'; }
}

document.getElementById('importVoc').addEventListener('click', importVoc);
document.getElementById('importTxt').addEventListener('click', importTxt);
loadImgClassDist();
loadImagesGrid();
</script>